/**
@file 	OpenGLGraph.cpp
@author Lime Microsystems
@brief	OpenGL based basic chart drawing
*/

#include "GL/glew.h" // must be first to be included before gl.h

#include "OpenGLGraph.h"
#include <cmath>
#include <iostream>
#include "GLFont.h"
#include "StaticFontHeader.h"
#include "dlgMarkers.h"
#include <wx/msgdlg.h>
#include <wx/timer.h>
using namespace std;

constexpr bool OGL_REDRAW_ENABLED{ true };
constexpr bool OGL_INVERT_MOUSE_Y{ true };

const unsigned int OpenGLGraph::mMarkerColors[] = {
    0x000000FF, 0x0000FFFF, 0xFF0000FF, 0xFF7F00FF, 0x007FFFFF, 0xFF00FFFF, 0x007F00FF, 0x00007FFF, 0x7F0000FF, 0x00FF00FF
};
const long markers_timer_id = wxNewId();

const int OpenGLGraph::GLCanvasAttributes[8] = {
    WX_GL_RGBA, WX_GL_DOUBLEBUFFER, WX_GL_DEPTH_SIZE, 16, WX_GL_STENCIL_SIZE, 0, 0, 0
};

bool OpenGLGraph::hasNotRecentEnoughOpenGLVersionWarningBeenThrownYet = false;

static bool CanUseVBO()
{
#ifdef __GNUC__
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "-Wold-style-cast"
#endif
    return GLEW_VERSION_1_5;
#ifdef __GNUC__
    #pragma GCC diagnostic pop
#endif
}

GLG_settings::GLG_settings()
    : title(""s)
    , titleXaxis(""s)
    , titleYaxis(""s)
    , xUnits(""s)
    , yUnits(""s)
    , drawGridX(true)
    , drawGridY(true)
    , drawTitle(true)
    , drawTitleX(true)
    , drawTitleY(true)
    , windowWidth(100)
    , windowHeight(100)
    , dataViewWidth(95)
    , dataViewHeight(95)
    , marginTop(0)
    , marginBottom(10)
    , marginLeft(10)
    , marginRight(0)
    , useVBO(true)
    , backgroundColor(0xffffffff)
    , titlesColor(0x000000ff)
    , dataViewBackgroundColor(0xfdfdfdff)
    , dataViewPerimeterColor(0x00ff00ff)
    , gridColor(0xD0D0D0ff)
    , graphType(GLG_LINE)
    , visibleArea(-100, 100, -100, 100)
    , gridXstart(0)
    , gridYstart(0)
    , gridXspacing(1)
    , gridYspacing(1)
    , gridXlines(4)
    , gridYlines(4)
    , gridXprec(0)
    , gridYprec(0)
    , gridValuesHeight(14)
    , pointsSize(3)
    , fontSize(1)
    , staticGrid(false)
    , lock_aspect(false)
    , markersEnabled(false)
    , gridXoffset(0)
{
}

OpenGLGraph::OpenGLGraph(wxWindow* parent,
    wxWindowID id = -1,
    const wxPoint& pos = wxDefaultPosition,
    const wxSize& size = wxDefaultSize,
    long style = 0,
    const wxString& name,
    const int* args)
    : wxGLCanvas(parent, id, args, pos, size, wxNO_FULL_REPAINT_ON_RESIZE)
    , oglOk(true)
    , m_actionState(OGLG_IDLE)
    , initialized(false)
    , initialDisplayArea(-100, 100, -100, 100)
    , m_MouseCoord(0, 0, 0, 0)
    , m_font(nullptr)
    , m_selectedMarker(-1)
    , m_maxMarkers(10)
    , m_glContext(new wxGLContext(this))
{
    SetBackgroundStyle(wxBG_STYLE_CUSTOM);
    int w, h;
    GetSize(&w, &h);
    setupViewport(w, h);

    //Initialize(w, h);
    m_popmenu.Append(OGLG_SHOW_MARKERS_MENU, _("Markers menu.."), _("Shows markers settings"));
    m_popmenu.Append(OGLG_SEARCH_PEAK, _("Add marker on peak value"), _("Adds marker to peak value"));
    m_popmenu.Append(OGLG_ADD_MARK, _("Add marker"), _("Adds measuring marker"));
    m_popmenu.Append(OGLG_REMOVE_MARKER, _("Remove marker"), _("Removes measuring marker"));
    m_popmenu.Append(OGLG_FIT, _("Fit"), _("Set plot view to show all items"));
    m_popmenu.Append(OGLG_RESET, _("Reset"), _("Resets view to initial state"));
    m_popmenu.AppendCheckItem(OGLG_LOCKASPECT, _("Lock aspect"), _("Lock horizontal and vertical zoom aspect."));
    wxMenuItem* chkbox = m_popmenu.FindItem(OGLG_LOCKASPECT);
    chkbox->Check(settings.lock_aspect);
    m_popmenu.Append(OGLG_HELP_MOUSE, _("Show mouse commands..."), _("Show help about the mouse commands."));
    for (size_t i = 0; i < m_maxMarkers; ++i)
    {
        markers.push_back(OGLMarker());
        markers[i].color = mMarkerColors[i];
        markers[i].id = i;
    }

    mMarkersDlg = new dlgMarkers(this);
    mMarkersDlg->parent_graph = this;
    for (size_t i = 0; i < m_maxMarkers; ++i)
        mMarkersDlg->AddMarker(i);
    mMarkersDlg->AddDeltas();

    for (int i = 0; i < 3; ++i)
    {
        info_msg.push_back(""s);
        info_msg_toDisplay.push_back(""s);
    }

    m_timer = new wxTimer(this, markers_timer_id);
    m_timer->Start(500);
}

OpenGLGraph::~OpenGLGraph()
{
    if (m_timer->IsRunning())
    {
        m_timer->Stop();
    }
    if (m_font)
    {
        delete m_font;
    }
    if (m_glContext)
    {
        delete m_glContext;
    }
    if (m_timer)
    {
        delete m_timer;
    }

    for (auto serie : series)
    {
        if (serie != nullptr)
        {
            delete serie;
        }
    }
}

bool OpenGLGraph::Initialize(int width, int height)
{
    GLenum err = glewInit();
    if (GLEW_OK != err)
    {
        cout << "GLEW ERROR "sv << glewGetErrorString(err) << endl;
    }

    char userOGLversion[255];
    strcpy(userOGLversion, reinterpret_cast<const char*>(glGetString(GL_VERSION)));

    oglOk = glewIsSupported("GL_VERSION_2_0");

    if (!oglOk)
    {
        if (!hasNotRecentEnoughOpenGLVersionWarningBeenThrownYet)
        {
            hasNotRecentEnoughOpenGLVersionWarningBeenThrownYet = true;
            wxMessageBox(
                wxString::Format(
                    "Your OpenGL version is %s, required version is 2.0\nPlease update your graphics card drivers", userOGLversion),
                _("WARNING"),
                wxOK,
                this);
        }

        return false;
    }

    glEnable(GL_TEXTURE_2D);
    glAlphaFunc(GL_GEQUAL, 0.3);
    glEnable(GL_ALPHA_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glClearColor(settings.backgroundColor.red,
        settings.backgroundColor.green,
        settings.backgroundColor.blue,
        settings.backgroundColor.alpha);
    glClear(GL_COLOR_BUFFER_BIT);

    Resize(width, height);
    settings.fontSize = 10;

    viewChanged = true;
    initialized = true;

    m_font = new GLFont();
    m_font->loadFromArray(reinterpret_cast<const char*>(standardStaticFont), sizeof(standardStaticFont));
    return true;
}

/**
	@brief Changes graph view size.
	@param w window width
	@param h window height
*/
void OpenGLGraph::Resize(int w, int h)
{
    if (w <= 0 || h <= 0)
        return;
    settings.windowWidth = w * GetContentScaleFactor();
    settings.windowHeight = h * GetContentScaleFactor();
    settings.dataViewHeight = settings.windowHeight - settings.marginTop - settings.marginBottom;
    if (settings.dataViewHeight <= 0)
        settings.dataViewHeight = 1;
    settings.dataViewWidth = settings.windowWidth - settings.marginLeft - settings.marginRight;
    if (settings.dataViewWidth <= 0)
        settings.dataViewWidth = 1;
    SettingsChanged();
}

/**
	@brief Adds data serie to graph
*/
void OpenGLGraph::AddSerie(cDataSerie* serie)
{
    if (serie != nullptr)
        series.push_back(serie);
}

/**
	@brief Removes data serie from graph
	@param i index of data serie
*/
void OpenGLGraph::RemoveSeries(unsigned int i)
{
    if (i < series.size())
        series.erase(series.begin() + i);
}

/**
	@brief Sets initially displayed data area, when resetting zoom it will be reset to this area
	@param minx minimal x value
	@param maxx maximum x value
	@param miny minimum y value
	@param maxy maximum y value
*/
void OpenGLGraph::SetInitialDisplayArea(float minx, float maxx, float miny, float maxy)
{
    initialDisplayArea.set(minx, maxx, miny, maxy);
    SetDisplayArea(minx, maxx, miny, maxy);
}

/**
	@brief Sets visible area
	@param minx minimal x value
	@param maxx maximum x value
	@param miny minimum y value
	@param maxy maximum y value
*/
void OpenGLGraph::SetDisplayArea(float minx, float maxx, float miny, float maxy)
{
    if ((minx == maxx) || (miny == maxy))
        return;
    settings.visibleArea.set(minx, maxx, miny, maxy);
    SettingsChanged();
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

/**
	@brief Changes Y axis center and span values
	@param centerY Y axis value that will be in the center of the window
	@param spanY Y axis span around center value
*/
void OpenGLGraph::ZoomY(float centerY, float spanY)
{
    SetDisplayArea(settings.visibleArea.x1, settings.visibleArea.x2, centerY - spanY / 2, centerY + spanY / 2);
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

/**
	@brief Changes X axis center and span values
	@param centerX X axis value that will be in the center of the window
	@param spanX X axis span around center value
*/
void OpenGLGraph::ZoomX(float centerX, float spanX)
{
    SetDisplayArea(centerX - spanX / 2, centerX + spanX / 2, settings.visibleArea.y1, settings.visibleArea.y2);
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

/**
	@brief Changes X and Y axis center and span values
	@param centerX X axis value that will be in the center of the window
	@param centerY Y axis value that will be in the center of the window
	@param spanX X axis span around center value
	@param spanY Y axis span around center value
*/
void OpenGLGraph::Zoom(float centerX, float centerY, float spanX, float spanY)
{
    if (spanX > 1E-03 && spanY > 1E-03)
    {
        if (settings.lock_aspect)
        {
            if (spanX < spanY)
                spanX = spanY;
            else
                spanY = spanX;
        }
        SetDisplayArea(centerX - spanX / 2, centerX + spanX / 2, centerY - spanY / 2, centerY + spanY / 2);
    }
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

/**
	@brief Zooms in area selected by window pixels
	@param x1 first corner x pixel coordinate
	@param x2 second corner x pixel coordinate
	@param y1 first corner y pixel coordinate
	@param y2 second corner y pixel coordinate

	Coordinates (0;0) are at the lower left corner
*/
void OpenGLGraph::ZoomRect(int x1, int x2, int y1, int y2)
{
    float minx, maxx, miny, maxy;
    float temp;
    dataViewPixelToValue(x1 - settings.marginLeft, y1 - settings.marginBottom, minx, miny);
    dataViewPixelToValue(x2 - settings.marginLeft, y2 - settings.marginBottom, maxx, maxy);
    if (minx > maxx)
    {
        temp = maxx;
        maxx = minx;
        minx = temp;
    }
    if (miny > maxy)
    {
        temp = maxy;
        maxy = miny;
        miny = temp;
    }
    if (maxx - minx > 1E-03 && maxy - miny > 1E-03)
    {
        SetDisplayArea(minx, maxx, miny, maxy);
    }

    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

/**
	@brief Moves visible data area by set amount of pixels
	@param dx number of pixels to move on the x axis
	@param dy number of pixels to move on the y axis
*/
void OpenGLGraph::Pan(float dx, float dy)
{
    float deltaX = dx * (settings.visibleArea.x2 - settings.visibleArea.x1) / settings.dataViewWidth;
    float deltaY = dy * (settings.visibleArea.y2 - settings.visibleArea.y1) / settings.dataViewHeight;
    SetDisplayArea(settings.visibleArea.x1 + deltaX,
        settings.visibleArea.x2 + deltaX,
        settings.visibleArea.y1 + deltaY,
        settings.visibleArea.y2 + deltaY);
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

/**
	@brief Sets drawing mode ( GLG_POINTS, GLG_LINE )
*/
void OpenGLGraph::SetDrawingMode(eDrawingMode mode)
{
    settings.graphType = mode;
}

/**
	@brief Draw static elements (background colors, titles, grid)
*/
void OpenGLGraph::DrawStaticElements()
{
    switchToWindowView();
    //draw data view background
    glBegin(GL_QUADS);
    glColor4f(settings.dataViewBackgroundColor.red,
        settings.dataViewBackgroundColor.green,
        settings.dataViewBackgroundColor.blue,
        settings.dataViewBackgroundColor.alpha);
    glVertex3f(0, 0, -5);
    glVertex3f(settings.windowWidth, 0, -5);
    glVertex3f(settings.windowWidth, settings.windowHeight, -5);
    glVertex3f(0, settings.windowHeight, -5);
    glEnd();
    glFlush();

    //draw title
    int tw = 0, th = 0;
    float fheight = 32;

    glColor4f(settings.titlesColor.red, settings.titlesColor.green, settings.titlesColor.blue, settings.titlesColor.alpha);
    m_font->getTextSize(settings.title.c_str(), tw, th, fheight);
    glRenderText(settings.windowWidth / 2 + settings.marginLeft - tw / 2,
        settings.windowHeight - th - 1,
        0,
        fheight,
        0,
        "%s",
        settings.title.c_str());

    //draw axis titles and units
    glColor4f(settings.titlesColor.red, settings.titlesColor.green, settings.titlesColor.blue, settings.titlesColor.alpha);
    m_font->getTextSize(settings.titleXaxis.c_str(), tw, th, fheight);
    glRenderText(settings.windowWidth - tw - settings.marginRight - 1,
        settings.marginBottom + 1,
        0,
        fheight,
        0,
        "%s",
        settings.titleXaxis.c_str());
    m_font->getTextSize(settings.titleYaxis.c_str(), tw, th, fheight);
    glRenderText(settings.marginLeft + 1, settings.windowHeight - th - 1, 0, fheight, 0, "%s", settings.titleYaxis.c_str());

    double pixelXvalue = 0;
    double pixelYvalue = 0;
    pixelXvalue = (settings.visibleArea.x2 - settings.visibleArea.x1) / settings.dataViewWidth;
    pixelYvalue = (settings.visibleArea.y2 - settings.visibleArea.y1) / settings.dataViewHeight;

    if (viewChanged)
    {
        CalculateGrid();
        viewChanged = false;
    }

    char text[32];
    //draw axis lines
    glBegin(GL_LINES);
    glColor3f(0.0, 0.0, 0.0);
    glVertex2d(settings.marginLeft, settings.windowHeight);
    glVertex2d(settings.marginLeft, 0);
    glVertex2d(0, settings.marginBottom);
    glVertex2d(settings.windowWidth, settings.marginBottom);
    glEnd();

    //print X axis
    int posX = 0;
    int posY = 0;
    double value = 0;
    double gridXstart = settings.gridXstart;

    char format[10];
    if (settings.staticGrid)
        std::snprintf(format, sizeof(format), "%%.3f %%s");
    else
        std::snprintf(format, sizeof(format), "%%.%if %%s", settings.gridXprec);

    float numbersH = 16;
    // X axis grid lines
    posY = settings.marginBottom;
    posX = 0;
    float gridpx = gridXstart;

    for (int i = 0; i < settings.windowWidth && posX < settings.windowWidth - settings.marginRight; ++i)
    {
        posX = settings.marginLeft + ((gridpx - settings.visibleArea.x1) / pixelXvalue);
        if (posX > settings.marginLeft && posX < settings.windowWidth - settings.marginRight)
        {
            if (settings.staticGrid)
                value = gridpx / 1000000.0 + settings.gridXoffset;
            else
                value = gridpx + settings.gridXoffset;
            std::snprintf(text, sizeof(text), format, value, settings.xUnits.c_str());
            m_font->getTextSize(text, tw, th, numbersH);
            glRenderText(posX - tw / 2, posY - th - 2, 0, numbersH, 0, "%s", text);
        }
        gridpx += settings.gridXspacing;
    }

    //Y axis grid
    //print Y axis
    posX = 0;
    posY = 0;
    value = 0;
    double gridYstart = settings.gridYstart;

    std::snprintf(format, sizeof(format), "%%.%if %%s", settings.gridYprec);

    // Y axis grid lines
    posY = settings.marginBottom;
    posX = settings.marginLeft;
    float gridpy = gridYstart;

    for (int i = 0; i < settings.windowHeight && posY < settings.windowHeight - settings.marginTop; ++i)
    {
        posY = settings.marginBottom + ((gridpy - settings.visibleArea.y1) / pixelYvalue);
        if (posY > settings.marginBottom && posY < settings.windowHeight - settings.marginTop)
        {
            value = gridpy;
            std::snprintf(text, sizeof(text), format, value, settings.yUnits.c_str());
            m_font->getTextSize(text, tw, th, numbersH);
            glRenderText(posX - tw - 2, posY - th / 2, 0, numbersH, 0, "%s", text);
        }
        gridpy += settings.gridYspacing;
    }

    //change to data view and start drawing grid
    switchToDataView();

    glBegin(GL_LINES);
    glColor4f(settings.gridColor.red, settings.gridColor.green, settings.gridColor.blue, settings.gridColor.alpha);

    //draw x grid lines
    for (float linePos = settings.gridXstart; linePos <= settings.visibleArea.x2; linePos += settings.gridXspacing)
    {
        glVertex3d(linePos, settings.visibleArea.y1, -2);
        glVertex3d(linePos, settings.visibleArea.y2, -2);
    }

    //draw y grid lines
    for (float linePos = settings.gridYstart; linePos <= settings.visibleArea.y2; linePos += settings.gridYspacing)
    {
        glVertex3d(settings.visibleArea.x1, linePos, -2);
        glVertex3d(settings.visibleArea.x2, linePos, -2);
    }

    glEnd();
    glFlush();
}

/**
	@brief Calculates grid position and spacing values
*/
void OpenGLGraph::CalculateGrid()
{
    if (settings.staticGrid)
    {
        settings.gridXlines = 10;
        settings.gridYlines = 10;
        //settings.gridXprec = 1;
        //settings.gridYprec = 1;
        settings.gridXstart = settings.visibleArea.x1;
        settings.gridXspacing = (settings.visibleArea.x2 - settings.visibleArea.x1) / settings.gridXlines;
        settings.gridYstart = settings.visibleArea.y1;
        settings.gridYspacing = (settings.visibleArea.y2 - settings.visibleArea.y1) / settings.gridYlines;

        return;
    }
    const int mult[11] = { 1, 1, 2, 2, 4, 5, 5, 5, 5, 5, 5 };

    float interval = settings.visibleArea.x2 - settings.visibleArea.x1; // span of x axis
    if (settings.gridXlines > 0)
    {
        //estimate text width
        const float cWidth = 15; //~double actual width
        float absx1 = std::fabs(settings.visibleArea.x1);
        float absx2 = std::fabs(settings.visibleArea.x2);
        int valLen =
            3 + std::fabs(std::log10(
                    absx1 > absx2 ? absx2 : absx2)); //some extra spacing for separation between values, and for decimal point
        int tmpLines = 1 + settings.dataViewWidth / (valLen * cWidth);
        float divisor = 1; // current grid spacing
        int xlines = settings.gridXlines > tmpLines ? tmpLines : settings.gridXlines;
        settings.gridXprec = 0;
        //determine if grid spacing needs to be decreased
        while (interval / divisor < xlines)
        {
            divisor /= 10;
            settings.gridXprec++;
            tmpLines = settings.dataViewWidth / (++valLen * cWidth);
            xlines = xlines > tmpLines ? tmpLines : xlines;
        }
        while (interval / divisor > 10 * xlines)
            divisor *= 10;
        settings.gridXspacing = divisor * mult[static_cast<int>(0.5f + interval / xlines / divisor)];
        settings.gridXstart = static_cast<int>(settings.visibleArea.x1 / settings.gridXspacing) * settings.gridXspacing;
    }
    else
        settings.gridXspacing = interval;

    interval = settings.visibleArea.y2 - settings.visibleArea.y1; // span of y axis
    if (settings.gridYlines > 0)
    {
        int tmpLines = 1 + settings.dataViewHeight / (2 * settings.gridValuesHeight); //2x to be safe in double scaling steps
        float divisor = 1; // current grid spacing
        int ylines = settings.gridYlines > tmpLines ? tmpLines : settings.gridYlines;
        settings.gridYprec = 0;
        while (interval / divisor < ylines)
        {
            divisor /= 10;
            settings.gridYprec++;
        }
        while (interval / divisor > 10 * ylines)
            divisor *= 10;
        settings.gridYspacing = divisor * mult[static_cast<int>(0.5f + interval / ylines / divisor)];
        settings.gridYstart = static_cast<int>(settings.visibleArea.y1 / settings.gridYspacing) * settings.gridYspacing;
    }
    else
        settings.gridYspacing = interval;
    SettingsChanged();
}

/**
	@brief Draw graph data series
*/
void OpenGLGraph::Draw()
{
    if (!IsShownOnScreen())
    {
        return;
    }
    SetCurrent(*m_glContext);
    int w, h;
    if (oglOk == false)
    {
        GetSize(&w, &h);
        setupViewport(w, h);
        glClearColor(settings.backgroundColor.red,
            settings.backgroundColor.green,
            settings.backgroundColor.blue,
            settings.backgroundColor.alpha);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        SwapBuffers();
        return;
    }
    if (!initialized)
    {
        int w, h;
        GetSize(&w, &h);
        if (Initialize(w, h) == false)
            return;
    }
    GetSize(&w, &h);
    setupViewport(w, h);
    glLoadIdentity();

    switchToWindowView();
    glLoadIdentity();
    glClearColor(settings.backgroundColor.red,
        settings.backgroundColor.green,
        settings.backgroundColor.blue,
        settings.backgroundColor.alpha);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //draw static view elements
    DrawStaticElements();
    switchToWindowView();
    //while zomming in draw selection rectangle
    if (m_actionState == OGLG_ZOOMIN)
    {
        glColor3f(1.0, 0, 0);
        glBegin(GL_LINE_STRIP);
        glVertex2d(m_MouseCoord.x1, m_MouseCoord.y1);
        glVertex2d(m_MouseCoord.x2, m_MouseCoord.y1);
        glVertex2d(m_MouseCoord.x2, m_MouseCoord.y2);
        glVertex2d(m_MouseCoord.x1, m_MouseCoord.y2);
        glVertex2d(m_MouseCoord.x1, m_MouseCoord.y1);
        glEnd();
        glFlush();
    }
    //draw series data

    switchToDataView();

    if (settings.useVBO && CanUseVBO())
    {
        for (std::size_t i = 0; i < series.size(); i++)
        {
            glColor3f(series[i]->color.red, series[i]->color.green, series[i]->color.blue);
            if (!series[i]->values.empty() && series[i]->visible)
            {
                if (series[i]->vboIndex == 0) //check if data series buffer is initialized
                {
                    glGenBuffersARB(1, &series[i]->vboIndex);
                }
                //bind buffer for filling
                glBindBufferARB(GL_ARRAY_BUFFER_ARB, series[i]->vboIndex);
                if (series[i]->modified) //check if buffer needs to be modified
                {
                    glBufferDataARB(GL_ARRAY_BUFFER_ARB,
                        sizeof(cDataSerie::Coordinates) * series[i]->values.size(),
                        nullptr,
                        GL_DYNAMIC_DRAW_ARB);
                    glBufferDataARB(GL_ARRAY_BUFFER_ARB,
                        sizeof(cDataSerie::Coordinates) * series[i]->values.size(),
                        series[i]->values.data(),
                        GL_DYNAMIC_DRAW_ARB);
                    series[i]->modified = false;
                }
                glEnableClientState(GL_VERTEX_ARRAY);
                glEnableClientState(GL_COLOR);
                glVertexPointer(2, GL_FLOAT, 0, nullptr);
                if (settings.graphType == GLG_POINTS)
                {
                    glPointSize(settings.pointsSize);
                    glDrawArrays(GL_POINTS, 0, series[i]->values.size());
                }
                else
                {
                    glPointSize(1);
                    glDrawArrays(GL_LINE_STRIP, 0, series[i]->values.size());
                }
                glDisableClientState(GL_VERTEX_ARRAY);
                glDisableClientState(GL_COLOR);
                glBindBufferARB(GL_ARRAY_BUFFER, 0);
            }
        }
    }
    else //backup case if VBO is not supported
    {
        for (unsigned int i = 0; i < series.size(); i++)
        {
            glColor3f(series[i]->color.red, series[i]->color.green, series[i]->color.blue);
            if (!series[i]->values.empty() && series[i]->visible)
            {
                if (settings.graphType == GLG_POINTS)
                {
                    glPointSize(settings.pointsSize);
                    glBegin(GL_POINTS);
                }
                else
                {
                    glPointSize(1);
                    glBegin(GL_LINE_STRIP);
                }
                for (std::size_t j = 0; j < series[i]->values.size(); j++)
                {
                    glVertex3f(series[i]->values[j].x, series[i]->values[j].y, 1.0);
                }
                glEnd();
            }
        }
        glFlush();
    }
    //draw measuring markers
    DrawMarkers();
    switchToWindowView();
    int fontSz = 16;
    unsigned int clrs[]{ 0xFF000000, 0x0000FF00, 0x00FF0000 };
    for (size_t i = 0; i < info_msg_toDisplay.size(); ++i)
    {
        glRenderText(
            settings.marginLeft, settings.marginBottom + i * 20 + fontSz, 0, fontSz, clrs[i], "%s", info_msg_toDisplay[i].c_str());
    }
    SwapBuffers();
}

bool OpenGLGraph::SaveConfig(char* file)
{
    return false;
}
bool OpenGLGraph::LoadConfig(char* file)
{
    return false;
}

/**
	@brief Signals that settings have changed and needs to be reevaluated
*/
void OpenGLGraph::SettingsChanged()
{
    viewChanged = true;
}

/**
	@brief Changes Opengl viewport area to whole window
*/
void OpenGLGraph::switchToWindowView()
{
    glViewport(0, 0, settings.windowWidth, settings.windowHeight);
    glMatrixMode(GL_PROJECTION); // add perspective to scene
    glLoadIdentity(); // restore matrix to original state
    glOrtho(0, settings.windowWidth, 0, settings.windowHeight, -100, 100);
    glMatrixMode(GL_MODELVIEW);
}

/**
	@brief changes Opengl viewport area to data view region and changes scale
*/
void OpenGLGraph::switchToDataView()
{

    glViewport(settings.marginLeft,
        settings.marginBottom,
        settings.windowWidth - settings.marginRight - settings.marginLeft,
        settings.windowHeight - settings.marginTop - settings.marginBottom);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity(); // restore matrix to original state
    glOrtho(settings.visibleArea.x1, settings.visibleArea.x2, settings.visibleArea.y1, settings.visibleArea.y2, -100, 100);
    glMatrixMode(GL_MODELVIEW);
}

/**
	@brief Prints text in defined position
	@param posx x position of text lower left corner
	@param posy y position of text lower left corner
	@param angle angle in degrees counterclockwise to rotate text
	@param scale font scaling
    @param rgba rgba value
	@param fmt text format and parameters
*/
GLvoid OpenGLGraph::glRenderText(float posx, float posy, float angle, float scale, unsigned int rgba, const char* fmt, ...)
{
    char text[256]; // Holds our string
    va_list ap; // Pointer to list of arguments

    if (fmt == nullptr) // If there's no text
        return; // Do nothing

    va_start(ap, fmt); // Parses the string for variables
    std::vsnprintf(text, sizeof(text), fmt, ap); // And converts symbols to actual numbers
    va_end(ap); // Results are stored in text

    glPushMatrix();
    glTranslatef(posx, posy, 0);
    glRotatef(angle, 0, 0, 1);

    //if font has been loaded
    glEnable(GL_TEXTURE_2D);
    if (m_font != nullptr)
        m_font->render_textWorldSpace(text, 0, 0, scale, rgba);

    glPopMatrix();
    glDisable(GL_TEXTURE_2D);
}

/**
	@brief Resets displayed area to initial area.
*/
void OpenGLGraph::ResetView()
{
    SetDisplayArea(initialDisplayArea.x1, initialDisplayArea.x2, initialDisplayArea.y1, initialDisplayArea.y2);
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

void OpenGLGraph::Fit()
{
    float minx = -1, maxx = 1, miny = -1, maxy = 1;
    float valx, valy;
    for (unsigned i = 0; i < series.size() && series[i]->visible; ++i) //set boundary limits to first value
    {
        if (!series[i]->values.empty())
        {
            minx = maxx = series[i]->values[0].x;
            miny = maxy = series[i]->values[0].y;
            break;
        }
    }
    for (unsigned i = 0; i < series.size(); ++i)
    {
        for (std::size_t j = 0; j < series[i]->values.size() && series[i]->visible; ++j)
        {
            valx = series[i]->values[j].x;
            valy = series[i]->values[j].y;
            if (valx < minx)
                minx = valx;
            else if (valx > maxx)
                maxx = valx;
            if (valy < miny)
                miny = valy;
            else if (valy > maxy)
                maxy = valy;
        }
    }
    SetDisplayArea(minx, maxx, miny, maxy);
}

/**
	@brief Handles mouse button pressing events
	@param mouseButton mouse button code
	@param X x coordinate of mouse pointer inside graph window
	@param Y y coordinate of mouse pointer inside graph window

	Given mouse coordinates (0,0) are at top left corner of window
*/
void OpenGLGraph::OnMouseDown(int mouseButton, int X, int Y)
{
    const float scale = GetContentScaleFactor();
    X *= scale;
    Y *= scale;

    if constexpr (OGL_INVERT_MOUSE_Y)
    {
        Y = settings.windowHeight - Y;
    }
    m_MouseCoord.x1 = X;
    m_MouseCoord.y1 = Y;

    switch (mouseButton)
    {
    case OGLG_LEFT:
        //move markers
        if ((m_selectedMarker = clickedOnMarker(X, Y)) >= 0)
        {
            m_actionState = OGLG_MOVE_MARKER;
        }
        else //start zooming in
        {
            m_actionState = OGLG_ZOOMIN;
            m_MouseCoord.x2 = X;
            m_MouseCoord.y2 = Y;
        }
        break;
    case OGLG_MIDDLE: // change axis span
        m_actionState = OGLG_SCALE;
        m_lastSpanX = settings.visibleArea.x2 - settings.visibleArea.x1;
        m_lastSpanY = settings.visibleArea.y2 - settings.visibleArea.y1;
        m_MouseCoord.x2 = X;
        m_MouseCoord.y2 = Y;
        break;

    case OGLG_RIGHT: // move display area
        m_actionState = OGLG_PAN;
        m_MouseCoord.x2 = X;
        m_MouseCoord.y2 = Y;
        m_MouseCoord.x1 = X;
        m_MouseCoord.y1 = Y;
        break;
    }
}

/**
	@brief Handles mouse button releasing events
	@param mouseButton mouse button code
	@param X x coordinate of mouse pointer inside graph window
	@param Y y coordinate of mouse pointer inside graph window

	Given mouse coordinates (0,0) are at top left corner of window
*/
void OpenGLGraph::OnMouseUp(int mouseButton, int X, int Y)
{
    /* Coordinate within the graph window measured in the window (logical)
     * coordinate system (without device scaling applied). */
    const int X_window = X;
    const int Y_window = Y;

    const float scale = GetContentScaleFactor();
    X *= scale;
    Y *= scale;

    switch (mouseButton)
    {
    case OGLG_LEFT:
        if (m_actionState == OGLG_ZOOMIN)
        {
            m_actionState = OGLG_IDLE;
            if (isInsideDataView(X, Y)) //finish zooming in
            {
                ZoomRect(m_MouseCoord.x1, m_MouseCoord.x2, m_MouseCoord.y1, m_MouseCoord.y2);
                if constexpr (OGL_REDRAW_ENABLED)
                {
                    Refresh();
                }
            }
            else // if zoomed outside data view border, then reset to initial view
            {
                ResetView();
                if constexpr (OGL_REDRAW_ENABLED)
                {
                    Refresh();
                }
            }
        }
        break;
    case OGLG_MIDDLE:

        break;

    case OGLG_RIGHT:
        if ((m_MouseCoord.x2 == m_MouseCoord.x1) && (m_MouseCoord.y2 == m_MouseCoord.y1))
            ShowMenu(X_window, Y_window);
        break;
    }
    m_actionState = OGLG_IDLE;
}

/**
	@brief Handles mouse moving events
	@param X x coordinate of mouse pointer inside graph window
	@param Y y coordinate of mouse pointer inside graph window

	Given mouse coordinates (0,0) are at top left corner of window
*/
void OpenGLGraph::OnMouseMove(int X, int Y)
{
    const float scale = GetContentScaleFactor();
    X *= scale;
    Y *= scale;

    float spanx, spany, sx, sy;
    if constexpr (OGL_INVERT_MOUSE_Y)
    {
        Y = settings.windowHeight - Y;
    }
    switch (m_actionState)
    {
    case OGLG_IDLE:

        break;
    case OGLG_MOVE_MARKER:
        MoveMarker(m_selectedMarker, X);
        break;
    case OGLG_PAN:
        //if mouse leaves data view, stop panning
        if (!isInsideDataView(X, Y))
        {
            m_actionState = OGLG_IDLE;
            break;
        }
        m_MouseCoord.x2 = m_MouseCoord.x1 - X;
        m_MouseCoord.y2 = m_MouseCoord.y1 - Y;
        Pan(m_MouseCoord.x2, m_MouseCoord.y2);
        m_MouseCoord.x1 = X;
        m_MouseCoord.y1 = Y;
        break;
    case OGLG_ZOOMIN:
        //update coordinates for zoom in rectangle drawing
        m_MouseCoord.x2 = X;
        m_MouseCoord.y2 = Y;
        if constexpr (OGL_REDRAW_ENABLED)
        {
            Refresh();
        }
        break;
    case OGLG_SCALE:
        //change axis span while mouse is inside data view
        if (!isInsideDataView(X, Y))
        {
            m_actionState = OGLG_IDLE;
            break;
        }
        m_MouseCoord.x2 = X;
        m_MouseCoord.y2 = Y;
        spanx = settings.visibleArea.x2 - settings.visibleArea.x1;
        spany = settings.visibleArea.y2 - settings.visibleArea.y1;
        sx = 1 + static_cast<float>(m_MouseCoord.x1 - m_MouseCoord.x2) / settings.dataViewWidth;
        sy = 1 + static_cast<float>(m_MouseCoord.y1 - m_MouseCoord.y2) / settings.dataViewHeight;
        Zoom(settings.visibleArea.x1 + spanx / 2, settings.visibleArea.y1 + spany / 2, m_lastSpanX * sx, m_lastSpanY * sy);
        break;
    default:
        m_MouseCoord.x2 = X;
        m_MouseCoord.y2 = Y;
        break;
    }
}

/**
	@brief Calculates value pointed by pixel in data view
	@param x x coordinate of mouse pointer
	@param y y coordinate of mouse pointer
	@param &valX x axis data value pointed by mouse
	@param &valY y axis data value pointed by mouse

	x and y coordinate (0, 0) are at the data view lower left corner
*/
void OpenGLGraph::dataViewPixelToValue(int x, int y, float& valX, float& valY)
{
    float deltaX = (settings.visibleArea.x2 - settings.visibleArea.x1) / settings.dataViewWidth;
    float deltaY = (settings.visibleArea.y2 - settings.visibleArea.y1) / settings.dataViewHeight;

    valX = settings.visibleArea.x1 + deltaX * x;
    valY = settings.visibleArea.y1 + deltaY * y;
}

/**
	@brief Checks if pixel is inside data view
	@param X mouse x coordinate
	@param Y mouse y coordinate
	@returns
		- false : pixel is outside data view
		- true : pixel is inside data view
*/
bool OpenGLGraph::isInsideDataView(int X, int Y)
{
    if (X > settings.marginLeft && X < (settings.windowWidth - settings.marginRight))
    {
        if (Y > settings.marginTop && Y < (settings.windowHeight - settings.marginBottom))
            return true;
    }
    return false;
}

/**
	@brief Returns text string width in pixels
	@param *str pointer to char array
*/
int OpenGLGraph::TextWidthInPixels(const char* str)
{
    return m_font->string_width(str);
}

/**
	@brief Returns number text width in pixels
	@param num number
	@param prec number of decimal digits
*/
int OpenGLGraph::NumberWidthInPixels(float num, unsigned int prec)
{
    char format[20];
    std::snprintf(format, sizeof(format), "%%.%if", prec);
    char ctemp[20];
    std::snprintf(ctemp, sizeof(ctemp), format, num);
    return TextWidthInPixels(ctemp);
}

/**
    @brief Returns line height in pixels
*/
int OpenGLGraph::LineHeight()
{
    return m_font->lineHeight();
}

/**
	@brief Add marker at selected pixel
	@param posX mouse x coordinate in graph window
	@return marker id
*/
int OpenGLGraph::AddMarker(int posX)
{
    bool found = false;
    //check if allowed to add more markers
    //check if series have any data to mark
    if (!series[0]->values.empty())
    {
        OGLMarker* mark = nullptr;
        for (size_t i = 0; i < markers.size(); ++i)
            if (markers[i].used == false)
            {
                mark = &markers[i];
                break;
            }
        if (mark == nullptr)
            return -1;
        //calculate marker position in data view
        float pixelXvalue = (settings.visibleArea.x2 - settings.visibleArea.x1) / settings.dataViewWidth;
        mark->posX = settings.visibleArea.x1 + (posX - settings.marginLeft) * pixelXvalue;

        //find closest data point index
        for (std::size_t i = 1; i < series[0]->values.size(); ++i)
        {
            if (series[0]->values[i].x > mark->posX)
            {
                float toLeft = mark->posX - series[0]->values[i - 1].x;
                float toRight = series[0]->values[i].x - mark->posX;
                if (toRight < toLeft)
                {
                    mark->dataValueIndex = i;
                }
                else
                {
                    mark->dataValueIndex = i - 1;
                }
                found = true;
                break;
            }
        }
        if (!found) //if no closest point found, add marker to middle point
        {
            mark->dataValueIndex = series[0]->values.size() / 2;
        }
        mark->used = true;
        mark->show = true;
        mark->posX = series[0]->values[mark->dataValueIndex].x;
        mark->posY = series[0]->values[mark->dataValueIndex].y;
        mark->color = mMarkerColors[markers.size()];
        return mark->id;
    }
    return -1;
}

/**
	@brief Add marker at selected data x value
	@return marker id
*/
int OpenGLGraph::AddMarkerAtValue(float xValue)
{
    bool found = false;
    //check if series have any data to mark
    if (!series[0]->values.empty())
    {
        OGLMarker* mark = nullptr;
        for (size_t i = 0; i < markers.size(); ++i)
            if (markers[i].used == false)
            {
                mark = &markers[i];
                break;
            }
        if (mark == nullptr)
            return -1;
        mark->posX = xValue;

        //find closest data point index
        for (std::size_t i = 0; i < series[0]->values.size(); ++i)
        {
            if (series[0]->values[i].x >= mark->posX)
            {
                float toLeft = mark->posX - series[0]->values[i - 1].x;
                float toRight = series[0]->values[i].x - mark->posX;
                if (toRight < toLeft)
                {
                    mark->dataValueIndex = i;
                }
                else
                {
                    mark->dataValueIndex = i - 1;
                }
                found = true;
                break;
            }
        }
        if (!found) //if no closest point found, add marker to middle point
        {
            mark->dataValueIndex = series[0]->values.size() / 2;
        }
        mark->used = true;
        mark->posX = series[0]->values[mark->dataValueIndex].x;
        mark->posY = series[0]->values[mark->dataValueIndex].y;
        mark->color = mMarkerColors[markers.size()];
        mMarkersDlg->refreshMarkFreq = true;
        return mark->id;
    }
    return -1;
}

/**
	@brief Removes last added marker
*/
void OpenGLGraph::RemoveMarker()
{
    for (int i = markers.size() - 1; i >= 0; --i)
        if (markers[i].used == true)
        {
            markers[i].used = false;
            break;
        }
    Refresh();
}

void OpenGLGraph::RemoveMarker(int id)
{
    for (size_t i = 0; i < markers.size(); ++i)
    {
        if (markers[i].id == id)
        {
            markers[i].used = false;
            break;
        }
    }
    Refresh();
}

/**
	@brief Draw data markers
*/
void OpenGLGraph::DrawMarkers()
{
    if (series.empty() || !settings.markersEnabled || series[0] == nullptr || series[0]->values.empty())
    {
        return;
    }

    switchToWindowView();

    float pixelXvalue = (settings.visibleArea.x2 - settings.visibleArea.x1) / settings.dataViewWidth;
    float pixelYvalue = (settings.visibleArea.y2 - settings.visibleArea.y1) / settings.dataViewHeight;

    glBegin(GL_TRIANGLES);
    int posX, posY;
    for (unsigned i = 0; i < markers.size(); ++i)
    {
        if (markers[i].used == false)
            continue;
        for (unsigned int j = 0; j < series.size(); j++)
        {
            if (series[j]->values.empty() || !series[j]->visible)
            {
                continue;
            }

            markers[i].posY = series[j]->values[markers[i].dataValueIndex].y;
            // X axis grid lines
            posY =
                settings.marginBottom + ((series[j]->values[markers[i].dataValueIndex].y - settings.visibleArea.y1) / pixelYvalue);
            posX = settings.marginLeft + ((markers[i].posX - settings.visibleArea.x1) / pixelXvalue);
            markers[i].iposX = posX;
            markers[i].iposY = posY;
            markers[i].size = 10;

            if (posX < settings.marginLeft || posX > settings.windowWidth - settings.marginRight)
            {
                continue;
            }

            markers[i].color = mMarkerColors[i];
            glColor4f(markers[i].color.red, markers[i].color.green, markers[i].color.blue, markers[i].color.alpha);
            if (posY >= settings.marginBottom && posY <= settings.windowHeight - settings.marginTop)
            {
                glVertex3f(posX, posY, 10);
                glVertex3f(posX + markers[i].size, posY + markers[i].size, 10);
                glVertex3f(posX - markers[i].size, posY + markers[i].size, 10);
            }
            glVertex3f(posX, settings.marginBottom + markers[i].size, 10);
            glVertex3f(posX - markers[i].size, settings.marginBottom, 10);
            glVertex3f(posX + markers[i].size, settings.marginBottom, 10);
        }
    }
    glEnd();
    glFlush();

    //draw marker data at the right upper corner of data view
    char text[256];
    float textScale = 1;
    int hpos = textScale * m_font->lineHeight() * 2.5;
    for (unsigned i = 0; i < markers.size(); ++i)
    {
        if (markers[i].used == false)
            continue;
        glColor4f(markers[i].color.red, markers[i].color.green, markers[i].color.blue, markers[i].color.alpha);
        int cnt = std::snprintf(text, sizeof(text), "M%i: % .3f MHz ", i, series[0]->values[markers[i].dataValueIndex].x / 1000000);

        for (unsigned int j = 0; j < series.size(); j++)
            if (!series[j]->values.empty() && series[j]->visible)
                cnt += std::snprintf(text + cnt,
                    std::max<int>(sizeof(text) - cnt, 0),
                    "/ Ch %c: %#+3.1f dBFS ",
                    65 + j,
                    series[j]->values[markers[i].dataValueIndex].y);

        markers[i].posY = series[0]->values[markers[i].dataValueIndex].y;
        posX = settings.marginLeft;
        posY = settings.windowHeight - settings.marginTop - hpos;
        //glPrint(posX, posY, 0, textScale, "%s", text);
        if (markers[i].show == false)
            continue;
        hpos += textScale * m_font->lineHeight();
        glRenderText(posX, posY, 0, textScale * m_font->lineHeight(), markers[i].color.getColor4b(), "%s", text);
    }
}

/**
	@brief Checks if mouse clicked on marker
	@param X mouse x coorindate
	@param Y mouse y coorindate
	@returns clicked marker index
*/
int OpenGLGraph::clickedOnMarker(int X, int Y)
{
    for (unsigned i = 0; i < markers.size(); ++i)
    {
        if (markers[i].used == false || X <= markers[i].iposX - markers[i].size || X >= markers[i].iposX + markers[i].size)
        {
            continue;
        }

        if (Y > settings.marginBottom && Y < settings.marginBottom + markers[i].size)
        {
            printf("selected %i marker\n", i);
            return i;
        }

        for (unsigned int j = 0; j < series.size(); j++)
        {
            if (series[j]->values.empty() || !series[j]->visible)
            {
                continue;
            }

            float pixelYvalue = (settings.visibleArea.y2 - settings.visibleArea.y1) / settings.dataViewHeight;
            int posY =
                settings.marginBottom + ((series[j]->values[markers[i].dataValueIndex].y - settings.visibleArea.y1) / pixelYvalue);
            if (Y > posY && Y < posY + markers[i].size)
            {
                printf("selected %i marker\n", i);
                return i;
            }
        }
    }
    return -1;
}

/**
	@brief change marker position
	@param markerID index of marker to be moved
	@param posX mouse x coordinate in graph window
*/
void OpenGLGraph::MoveMarker(int markerID, int posX)
{
    if (series[0]->values.empty() || markerID < 0)
    {
        mMarkersDlg->refreshMarkFreq = true;
        if constexpr (OGL_REDRAW_ENABLED)
        {
            Refresh();
        }
        return;
    }

    if (markers[markerID].used == false)
        return;

    float tempY = 0;
    dataViewPixelToValue(posX - settings.marginLeft, 0, markers[markerID].posX, tempY);

    //determine which way marker was moved, search for closest data point
    if (posX < markers[markerID].iposX)
    {
        for (int i = markers[markerID].dataValueIndex; i > 0; --i)
        {
            if (series[0]->values[i].x > markers[markerID].posX)
            {
                continue;
            }

            float toLeft = markers[markerID].posX - series[0]->values[i - 1].x;
            float toRight = series[0]->values[i].x - markers[markerID].posX;
            if (toRight < toLeft)
            {
                markers[markerID].dataValueIndex = i;
            }
            else
            {
                markers[markerID].dataValueIndex = i - 2;
            }
            break;
        }
    }
    else
    {
        for (std::size_t i = markers[markerID].dataValueIndex; i < series[0]->values.size(); ++i)
        {
            if (series[0]->values[i].x < markers[markerID].posX)
            {
                continue;
            }

            float toLeft = markers[markerID].posX - series[0]->values[i - 1].x;
            float toRight = series[0]->values[i].x - markers[markerID].posX;
            if (toRight < toLeft)
            {
                markers[markerID].dataValueIndex = i;
            }
            else
            {
                markers[markerID].dataValueIndex = i - 2;
            }
            break;
        }
    }
    float pixelXvalue = (settings.visibleArea.x2 - settings.visibleArea.x1) / settings.dataViewWidth;
    float pixelYvalue = (settings.visibleArea.y2 - settings.visibleArea.y1) / settings.dataViewHeight;
    markers[markerID].posX = series[0]->values[markers[markerID].dataValueIndex].x;
    markers[markerID].posY = series[0]->values[markers[markerID].dataValueIndex].y;
    int posY =
        settings.marginBottom + ((series[0]->values[markers[markerID].dataValueIndex].y - settings.visibleArea.y1) / pixelYvalue);
    int newPosX = settings.marginLeft + ((markers[markerID].posX - settings.visibleArea.x1) / pixelXvalue);
    markers[markerID].iposX = newPosX;
    markers[markerID].iposY = posY;

    mMarkersDlg->refreshMarkFreq = true;
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

/**
	@brief change marker position
	@param markerID index of marker to be moved
	@param xValue mouse x coordinate in graph window
*/
void OpenGLGraph::ChangeMarker(int markerID, float xValue)
{
    if (series[0]->values.empty() || markerID < 0)
    {
        Refresh();
        if constexpr (OGL_REDRAW_ENABLED)
        {
            Refresh();
        }
        return;
    }

    OGLMarker* mark = nullptr;
    for (size_t i = 0; i < markers.size(); ++i)
        if (markers[i].id == markerID)
            mark = &markers[i];
    if (mark == nullptr)
        return;

    mark->posX = xValue;
    bool found = false;
    //find closest data point index
    for (std::size_t i = 1; i < series[0]->values.size(); ++i)
    {
        if (series[0]->values[i].x < mark->posX)
        {
            continue;
        }

        float toLeft = mark->posX - series[0]->values[i - 1].x;
        float toRight = series[0]->values[i].x - mark->posX;
        if (toRight < toLeft)
        {
            mark->dataValueIndex = i;
        }
        else
        {
            mark->dataValueIndex = i - 1;
        }
        found = true;
        break;
    }
    if (!found) //if no closest point found, add marker to middle point
    {
        mark->dataValueIndex = series[0]->values.size() / 2;
    }
    mark->posX = series[0]->values[mark->dataValueIndex].x;
    mark->posY = series[0]->values[mark->dataValueIndex].y;

    Refresh();
    if constexpr (OGL_REDRAW_ENABLED)
    {
        Refresh();
    }
}

void OpenGLGraph::setupViewport(int w, int h)
{
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Black Background
    glEnable(GL_DEPTH_TEST);

    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    glOrtho(0, w, h, 0, -1, 1);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

void OpenGLGraph::render(wxPaintEvent& evt)
{
    Draw();
}

void OpenGLGraph::resized(wxSizeEvent& evt)
{
    wxSize wh;
    wh = evt.GetSize();
    Resize(wh.GetWidth(), wh.GetHeight());
    Refresh();
}

void OpenGLGraph::mouseMoved(wxMouseEvent& event)
{
    OnMouseMove(event.GetX(), event.GetY());
}

void OpenGLGraph::mouseWheelMoved(wxMouseEvent& event)
{
}

void OpenGLGraph::mouseReleased(wxMouseEvent& event)
{
    if (event.LeftUp())
        OnMouseUp(OGLG_LEFT, event.GetX(), event.GetY());
    else if (event.RightUp())
        OnMouseUp(OGLG_RIGHT, event.GetX(), event.GetY());
    else if (event.MiddleUp())
        OnMouseUp(OGLG_MIDDLE, event.GetX(), event.GetY());
}
void OpenGLGraph::rightClick(wxMouseEvent& event)
{
    OnMouseDown(OGLG_RIGHT, event.GetX(), event.GetY());
}
void OpenGLGraph::leftClick(wxMouseEvent& event)
{
    OnMouseDown(OGLG_LEFT, event.GetX(), event.GetY());
}
void OpenGLGraph::middleClick(wxMouseEvent& event)
{
    OnMouseDown(OGLG_MIDDLE, event.GetX(), event.GetY());
}
void OpenGLGraph::mouseLeftWindow(wxMouseEvent& event)
{
}
void OpenGLGraph::keyPressed(wxKeyEvent& event)
{
}
void OpenGLGraph::keyReleased(wxKeyEvent& event)
{
}

void OpenGLGraph::onFit(wxCommandEvent& event)
{
    Fit();
}

void OpenGLGraph::onMouseHelp(wxCommandEvent& event)
{
    wxMessageBox(_("Supported Mouse commands:\n \
        - Left button down + Mark area: Rectangular zoom\n \
        - Right button down + Move: Pan (Move)\n \
        - Wheel button down + Move: vertical & horizontal zooming\n"),
        _("plot controls"),
        wxOK,
        this);
}

void OpenGLGraph::onAddMarker(wxCommandEvent& event)
{
    AddMarker(m_MouseCoord.x2);
    Refresh();
}
void OpenGLGraph::onRemoveMarker(wxCommandEvent& event)
{
    RemoveMarker();
    Refresh();
}

void OpenGLGraph::onShowMarkersMenu(wxCommandEvent& event)
{
    if (m_timer->IsRunning() == false)
        m_timer->Start(500);
    mMarkersDlg->Show();
    mMarkersDlg->refreshMarkFreq = true;
}

void OpenGLGraph::onLockAspect(wxCommandEvent& event)
{
    settings.lock_aspect = event.GetInt();
}

void OpenGLGraph::ShowMenu(int x, int y)
{
    //modify menu
    wxMenuItem* item = nullptr;
    if (settings.markersEnabled == false)
    {
        item = m_popmenu.FindItem(OGLG_ADD_MARK);
        item->Enable(false);
        item = m_popmenu.FindItem(OGLG_REMOVE_MARKER);
        item->Enable(false);
        item = m_popmenu.FindItem(OGLG_SEARCH_PEAK);
        item->Enable(false);
        item = m_popmenu.FindItem(OGLG_SHOW_MARKERS_MENU);
        item->Enable(false);
    }
    PopupMenu(&m_popmenu, x, y);
}

void OpenGLGraph::onSearchPeak(wxCommandEvent& event)
{
    SearchPeak();
    Refresh();
    mMarkersDlg->refreshMarkFreq = true;
}

void OpenGLGraph::onReset(wxCommandEvent& event)
{
    ResetView();
    Refresh();
}

bool OpenGLGraph::SearchPeak()
{
    float maxValue{ std::numeric_limits<float>::lowest() };
    float maxX{ 0 };

    //Find max position
    for (const auto* const serie : series)
    {
        if (!serie->values.empty() && serie->visible)
        {
            for (const auto& value : serie->values)
            {
                if (maxValue < value.y)
                {
                    maxX = value.x;
                    maxValue = value.y;
                }
            }
        }
    }

    //Mark max position
    for (const auto* const serie : series)
    {
        if (!serie->values.empty() && serie->visible)
        {
            AddMarkerAtValue(maxX);
            return true;
        }
    }
    return false;
}

void OpenGLGraph::SetMarker(int id, float xValue, bool enabled, bool show)
{
    if (id >= 0 && id < static_cast<int>(markers.size()))
    {
        ChangeMarker(id, xValue);
        markers[id].used = enabled;
        markers[id].show = show;
    }
}

void OpenGLGraph::GetMarker(int id, float& xValue, float& yValue, bool& enabled, bool& show)
{
    if (id >= 0 && id < static_cast<int>(markers.size()))
    {
        xValue = markers[id].posX;
        yValue = markers[id].posY;
        enabled = markers[id].used;
        show = markers[id].show;
    }
}

void OpenGLGraph::OnTimer(wxTimerEvent& event)
{
    mMarkersDlg->UpdateValues();
    UpdateInfoDisplay();
}

void OpenGLGraph::UpdateInfoDisplay()
{
    for (size_t i = 0; i < info_msg.size(); ++i)
    {
        info_msg_toDisplay[i] = info_msg[i];
    }
}

void OpenGLGraph::SetInfoMessage(const char* msg, unsigned int index)
{
    if (index < info_msg.size())
        info_msg[index] = msg;
}

BEGIN_EVENT_TABLE(OpenGLGraph, wxGLCanvas)
EVT_MOTION(OpenGLGraph::mouseMoved)
EVT_RIGHT_DOWN(OpenGLGraph::rightClick)
EVT_RIGHT_UP(OpenGLGraph::mouseReleased)
EVT_LEFT_DOWN(OpenGLGraph::leftClick)
EVT_LEFT_UP(OpenGLGraph::mouseReleased)
EVT_MIDDLE_UP(OpenGLGraph::mouseReleased)
EVT_MIDDLE_DOWN(OpenGLGraph::middleClick)
EVT_LEAVE_WINDOW(OpenGLGraph::mouseLeftWindow)
EVT_SIZE(OpenGLGraph::resized)
EVT_MOUSEWHEEL(OpenGLGraph::mouseWheelMoved)
EVT_PAINT(OpenGLGraph::render)
EVT_TIMER(wxID_ANY, OpenGLGraph::OnTimer)

EVT_MENU(OGLG_FIT, OpenGLGraph::onFit)
EVT_MENU(OGLG_HELP_MOUSE, OpenGLGraph::onMouseHelp)
EVT_MENU(OGLG_ADD_MARK, OpenGLGraph::onAddMarker)
EVT_MENU(OGLG_REMOVE_MARKER, OpenGLGraph::onRemoveMarker)
EVT_MENU(OGLG_SHOW_MARKERS_MENU, OpenGLGraph::onShowMarkersMenu)
EVT_MENU(OGLG_LOCKASPECT, OpenGLGraph::onLockAspect)
EVT_MENU(OGLG_SEARCH_PEAK, OpenGLGraph::onSearchPeak)
EVT_MENU(OGLG_RESET, OpenGLGraph::onReset)
END_EVENT_TABLE()
